//
//  PlayCore.swift
//  GhostWorld
//
//  Created by Kam on 2018/4/9.
//  Copyright © 2018 LKGood. All rights reserved.
//

import UIKit
import AVFoundation
import MediaPlayer

protocol PlayCoreProgressDelegate {
  func trackCachingProgress(bytesDownloaded: Int, bytesExpected: Int)
  func trackPlayingProgress(duration: Double, time: Double, percentage: Float)
}

protocol PlayCorePlaybackDelegate {
  func didStartPlaying()
  func didPausePlaying()
  func didFinishPlaying()
}

class PlayCore: NSObject {
  
  typealias PlayCorePrepareHandler = (Bool) -> Void
  
  // MARK: - Singleton
  static let shared = PlayCore()
  override init() {
    super.init()
    NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying), name: .AVPlayerItemDidPlayToEndTime, object: nil)
  }
  
  // MARK: - Private vars
  
  internal var player: AVPlayer?
  internal var playerItem: CachingPlayerItem?
  
  private var timeObserverToken: Any?
  private var cacheFileName: String?
  
  private var playerItemContext = 0
  private var playerContext = 1
  
  // MARK: - Private methods
  
  internal func cmTime(seconds: Double) -> CMTime {
    return CMTime(seconds: seconds, preferredTimescale: Int32(NSEC_PER_SEC)) // NSEC_PER_SEC: nanoseconds per second
  }
  
  @objc private func playerDidFinishPlaying() {
    playbackDelegate?.didFinishPlaying()
    NowPlayingManager.shared.setNowPlayingElapsedPlaybackTime (
      duration: playerItem!.duration.seconds,
      seconds: playerItem!.duration.seconds,
      rate: 0
    )
    
    // try play next audio in list
    if NowPlayingManager.shared.canPlayNextAuidoInList {
      NowPlayingManager.shared.playNextAudioInList(byUser: false)
    } else {
      NowPlayingManager.shared.clear()
      NotificationCenter.default.post(name: .finishedAudioPlayingInList, object: nil)
    }
  }
  
  private func seek(toTime: CMTime) {
    if playerItem != nil {
      playerItem!.seek(to: toTime) { success in
        if success {
          NowPlayingManager.shared.setNowPlayingElapsedPlaybackTime (
            duration: self.playerItem!.duration.seconds,
            seconds: toTime.seconds,
            rate: 1.0
          )
          loggingPrint("Seek to \(toTime.seconds)/\(self.playerItem!.duration.seconds)")
        } else {
          loggingPrint("Seek to \(toTime.seconds) failed")
        }
      }
    }
  }
  
  private func playPlayer() {
    player!.play()
    playbackDelegate?.didStartPlaying()
    NowPlayingManager.shared.resumePlayTimer()
    
    NowPlayingManager.shared.setNowPlayingElapsedPlaybackTime (
      duration: playerItem!.duration.seconds,
      seconds: playerItem!.currentTime().seconds,
      rate: 1.0
    )
  }
  
  private func pausePlayer() {
    player!.pause()
    playbackDelegate?.didPausePlaying()
    NowPlayingManager.shared.pausePlayTimer()
    
    NowPlayingManager.shared.setNowPlayingElapsedPlaybackTime (
      duration: playerItem!.duration.seconds,
      seconds: playerItem!.currentTime().seconds,
      rate: 0.0
    )
  }
  
  // MARK: - Audio session related
  
  private func activateAudioSession() {
    let session = AVAudioSession.sharedInstance()
    do {
      try session.setActive(true, options: [AVAudioSession.SetActiveOptions.notifyOthersOnDeactivation])
      loggingPrint("Application audio session activated")
    } catch {
      loggingPrint("Activate application audio session failed!")
    }
    NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: session)
    NotificationCenter.default.removeObserver(self, name: AVAudioSession.mediaServicesWereResetNotification, object: session)
    NotificationCenter.default.removeObserver(self, name: AVAudioSession.mediaServicesWereLostNotification, object: session)
    
    NotificationCenter.default.addObserver (
      self,
      selector: #selector(handleInterruption(notification:)),
      name: AVAudioSession.interruptionNotification,
      object: session
    )
    NotificationCenter.default.addObserver (
      self,
      selector: #selector(handleAusiosessionReset),
      name: AVAudioSession.mediaServicesWereResetNotification,
      object: session
    )
    NotificationCenter.default.addObserver (
      self,
      selector: #selector(handleAusiosessionLost),
      name: AVAudioSession.mediaServicesWereLostNotification,
      object: session
    )
  }
  
  @objc private func handleInterruption(notification: Notification) {
    if let typeRawValue = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt,
      let type = AVAudioSession.InterruptionType(rawValue: typeRawValue) {
      
      switch type {
      case .began:
        playbackDelegate?.didPausePlaying()
        loggingPrint("AudioSession interruption began")
      case .ended:
        activateAudioSession()
        
        if let optionRawValue = notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? UInt {
          let option = AVAudioSession.InterruptionOptions(rawValue: optionRawValue)
          if option == .shouldResume {
            playPlayer()
          }
        }
        loggingPrint("AudioSession interruption ended")
      }
    }
  }
  
  @objc private func handleAusiosessionReset() {
    loggingPrint("AudioSession reset")
  }
  
  @objc private func handleAusiosessionLost() {
    loggingPrint("AudioSession lost")
  }
  
  // MARK: - Public vars
  
  var progressDelegate: PlayCoreProgressDelegate?
  var playbackDelegate: PlayCorePlaybackDelegate?
  var prepareHandler: PlayCorePrepareHandler?
  
  var isPlaying: Bool {
    get {
      if player != nil {
        switch player!.timeControlStatus {
        case .paused:
          return false
          
        case .playing:
          return true
          
        case .waitingToPlayAtSpecifiedRate:
          return false
        }
      } else {
        return false
      }
    }
  }
  
  // MARK: - Lifecycle
  
  override func observeValue (
    forKeyPath keyPath: String?,
    of object: Any?,
    change: [NSKeyValueChangeKey : Any]?,
    context: UnsafeMutableRawPointer?
    ) {
    
    if context == &playerItemContext {
      // player item
      if keyPath == #keyPath(AVPlayerItem.status) {
        let status: AVPlayerItem.Status
        if let statusNumber = change?[.newKey] as? NSNumber {
          status = AVPlayerItem.Status(rawValue: statusNumber.intValue)!
        } else {
          status = .unknown
        }
        switch status {
        case .readyToPlay:
          prepareHandler?(true)
        case .failed:
          prepareHandler?(false)
        case .unknown:
          prepareHandler?(false)
        }
      }
    } else if context == &playerContext {
      // player
//      if keyPath == #keyPath(AVPlayer.status) {
//        let status: AVPlayerStatus
//        if let statusNumber = change?[.newKey] as? NSNumber {
//          status = AVPlayerStatus(rawValue: statusNumber.intValue)!
//        } else {
//          status = .unknown
//        }
//        
//        switch status {
//        case .readyToPlay:
//
//        case .failed:
//
//        case .unknown:
//          
//        }
//      }
    }
  }
  
  deinit {
    NotificationCenter.default.removeObserver(self)
  }
  
  // MARK: - Public methods
  
  func prepare(url: URL, cacheFileName: String, completion: @escaping PlayCorePrepareHandler) {
    prepareHandler = completion
    
    self.cacheFileName = cacheFileName
    
    if let localURL = NowPlayingManager.shared.audio?.localURL {
      do {
        let data = try Data(contentsOf: localURL)
        
        let lastPathComponent = localURL.lastPathComponent
        var fileExtension = ""
        if let index = lastPathComponent.index(of: ".") {
          fileExtension = lastPathComponent.substring(from: index + 1)
        }
        
        // play local audio
        playerItem = CachingPlayerItem(data: data, mimeType: "audio/mpeg", fileExtension: fileExtension)
        progressDelegate?.trackCachingProgress(bytesDownloaded: 1, bytesExpected: 1)
      } catch {
        // streaming online audio
        playerItem = CachingPlayerItem(url: url)
      }
    } else {
      // streaming online audio
      playerItem = CachingPlayerItem(url: url)
    }
    
    func prepare() {
      // player item
      playerItem!.delegate = self
      playerItem!.addObserver(
        self,
        forKeyPath: #keyPath(AVPlayerItem.status),
        options: [.old, .new],
        context: &playerItemContext)
      
      // player
      
      // remove current player time observer
      if let token = timeObserverToken {
        player?.removeTimeObserver(token)
        timeObserverToken = nil
      }
      
      // create new player
      player = AVPlayer(playerItem: playerItem)
      player!.automaticallyWaitsToMinimizeStalling = false
      player!.addObserver (
        self,
        forKeyPath: #keyPath(AVPlayer.status),
        options: [.old, .new],
        context: &playerContext
      )
      
      // start track playing progress
      let interval = CMTimeMakeWithSeconds(1, preferredTimescale: Int32(NSEC_PER_SEC))
      timeObserverToken = self.player!.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main) { time in
        let duration = CMTimeGetSeconds(self.playerItem!.duration)
        if duration > 0 {
          let percentage = Float((CMTimeGetSeconds(time) / duration))
          let currentTime = self.playerItem!.currentTime().seconds
          self.progressDelegate?.trackPlayingProgress (
            duration: self.playerItem!.duration.seconds,
            time: currentTime,
            percentage: percentage
          )
          
          if let audio = NowPlayingManager.shared.audio {
            audio.savePlayProgress(percentage)
            NotificationCenter.default.post(name: .updateNowPlayingProgress, object: nil)
          }
        }
      }
    }
    
    if player == nil {
      prepare()
    } else {
      player!.pause()
      prepare()
    }
  }
  
  func startPlay() {
    if player != nil && playerItem != nil {
      activateAudioSession()
      playPlayer()
      prepareHandler = nil
    }
  }
  
  func seek(toProgress: Float) {
    if playerItem != nil {
      let seconds = playerItem!.duration.seconds * Double(toProgress)
      seek(toTime: cmTime(seconds: seconds))
    }
  }
  
  func playPause() {
    if player != nil {
      if isPlaying {
        pausePlayer()
      } else {
        activateAudioSession()
        playPlayer()
      }
    }
  }
  
  func pauseForAutoSleep() {
    if player != nil {
      if isPlaying {
        player!.pause()
        playbackDelegate?.didPausePlaying()
        NowPlayingManager.shared.pausePlayTimer()
      }
      
      NowPlayingManager.shared.setNowPlayingElapsedPlaybackTime (
        duration: playerItem!.duration.seconds,
        seconds: playerItem!.currentTime().seconds,
        rate: 0.0
      )
    }
  }
  
  func skipForward15Seconds() {
    if playerItem != nil {
      var seekToTime = playerItem!.currentTime() + cmTime(seconds: 15)
      if seekToTime.seconds >= playerItem!.duration.seconds {
        seekToTime = playerItem!.duration
      }
      seek(toTime: seekToTime)
    }
  }
  
  func skipBackward15Seconds() {
    if playerItem != nil {
      var seekToTime = playerItem!.currentTime() - cmTime(seconds: 15)
      if seekToTime <= cmTime(seconds: 0) {
        seekToTime = cmTime(seconds: 0)
      }
      seek(toTime: seekToTime)
    }
  }
}

extension PlayCore: CachingPlayerItemDelegate {
  
  func playerItem(_ playerItem: CachingPlayerItem, didFinishDownloadingData data: Data) {
    if let cachePath = LocalFileManager.shared.cacheRootPath,
      let count = cacheFileName?.count, count > 0 {
      
      let cacheURL = URL(fileURLWithPath: "\(cachePath)/\(cacheFileName!)")
      
      do {
        try data.write(to: cacheURL, options: .atomicWrite)
        loggingPrint("Audio cache saved. \(cachePath)/\(cacheFileName!)")
        DispatchQueue.main.async {
          NotificationCenter.default.post (
            name: .streamingCacheFinished,
            object: nil,
            userInfo: nil
          )
        }
      } catch let error {
        loggingPrint(error)
      }
    }
  }
  
  func playerItem(_ playerItem: CachingPlayerItem, didDownloadBytesSoFar bytesDownloaded: Int, outOf bytesExpected: Int) {
    DispatchQueue.main.async {
      self.progressDelegate?.trackCachingProgress(bytesDownloaded: bytesDownloaded, bytesExpected: bytesExpected)
    }
  }
  
  func playerItem(_ playerItem: CachingPlayerItem, downloadingFailedWith error: Error) {
    loggingPrint(error)
  }
}
